﻿using Microsoft.AspNet.Identity;
using Sidvall.IdentityManager.Data;
using Sidvall.IdentityManager.Security.Entities;
using Sidvall.Serialization;
using System;
using System.Threading.Tasks;

namespace Sidvall.IdentityManager.Security
{
    public class UserTokenProvider : IUserTokenProvider, Microsoft.AspNet.Identity.IUserTokenProvider<Entities.AspNetUsers, string>
    {
        #region Public Members

        #region GenerateAsync

        public virtual Task<string> GenerateAsync(string purpose, UserManager<AspNetUsers, string> manager, AspNetUsers user)
        {
            return GenerateAsync(purpose, user);
        }
        public virtual Task<string> GenerateAsync(string purpose, IAspNetUsers user)
        {
            var securityToken = CreateSecurityToken(user);
            var modifier = GetUserModifier(purpose, user);
            var token = Rfc6238AuthenticationService.GenerateCode(securityToken, modifier).ToString("D6", System.Globalization.CultureInfo.InvariantCulture);
            return Task.FromResult(token);
        }

        #endregion
        #region IsValidProviderForUserAsync

        public virtual async Task<bool> IsValidProviderForUserAsync(IAspNetUsers user)
        {
            return await IsValidProviderForUserAsync(Sidvall.IdentityManager.Security.SystemContext.Current.UserManager, null).ConfigureAwait(false);
        }
        public virtual Task<bool> IsValidProviderForUserAsync(UserManager<AspNetUsers, string> manager, AspNetUsers user)
        {
            if (manager == null)
                throw new ArgumentNullException(nameof(manager));
            return Task.FromResult(manager.SupportsUserSecurityStamp);
        }

        #endregion
        #region NotifyAsync

        public virtual Task NotifyAsync(string token, UserManager<AspNetUsers, string> manager, AspNetUsers user)
        {
            return NotifyAsync(token, user);
        }
        public virtual Task NotifyAsync(string token, IAspNetUsers user)
        {
            return Task.FromResult(0);
        }

        #endregion
        #region ValidateAsync

        public virtual Task<bool> ValidateAsync(string purpose, string token, UserManager<AspNetUsers, string> manager, AspNetUsers user)
        {
            return ValidateAsync(purpose, token, user);
        }
        public virtual Task<bool> ValidateAsync(string purpose, string token, IAspNetUsers user)
        {
            var code = token.ToNullableInt32();
            if (code == null)
                return Task.FromResult(false);
            var securityToken = CreateSecurityToken(user);
            if (securityToken == null)
                return Task.FromResult(false);
            var modifier = GetUserModifier(purpose, user);
            var isValid = Rfc6238AuthenticationService.ValidateCode(securityToken, code.Value, modifier);
            return Task.FromResult(isValid);
        }

        #endregion

        #endregion
        #region Private Members

        #region Rfc6238AuthenticationService

        private static class Rfc6238AuthenticationService
        {
            private static readonly DateTime _unixEpoch = new DateTime(1970, 1, 1, 0, 0, 0, DateTimeKind.Utc);
            private static readonly TimeSpan _timestep = TimeSpan.FromMinutes(15);
            private static readonly System.Text.Encoding _encoding = new System.Text.UTF8Encoding(false, true);

            private static int ComputeTotp(System.Security.Cryptography.HashAlgorithm hashAlgorithm, ulong timestepNumber, string modifier)
            {
                // # of 0's = length of pin
                const int mod = 1000000;

                // See https://tools.ietf.org/html/rfc4226
                // We can add an optional modifier
                var timestepAsBytes = BitConverter.GetBytes(System.Net.IPAddress.HostToNetworkOrder((long)timestepNumber));
                var hash = hashAlgorithm.ComputeHash(ApplyModifier(timestepAsBytes, modifier));

                // Generate DT string
                var offset = hash[hash.Length - 1] & 0xf;
                System.Diagnostics.Debug.Assert(offset + 4 < hash.Length);
                var binaryCode = (hash[offset] & 0x7f) << 24
                                 | (hash[offset + 1] & 0xff) << 16
                                 | (hash[offset + 2] & 0xff) << 8
                                 | (hash[offset + 3] & 0xff);

                return binaryCode % mod;
            }

            private static byte[] ApplyModifier(byte[] input, string modifier)
            {
                if (String.IsNullOrEmpty(modifier))
                {
                    return input;
                }

                var modifierBytes = _encoding.GetBytes(modifier);
                var combined = new byte[checked(input.Length + modifierBytes.Length)];
                Buffer.BlockCopy(input, 0, combined, 0, input.Length);
                Buffer.BlockCopy(modifierBytes, 0, combined, input.Length, modifierBytes.Length);
                return combined;
            }

            // More info: https://tools.ietf.org/html/rfc6238#section-4
            private static ulong GetCurrentTimeStepNumber()
            {
                var delta = DateTime.UtcNow - _unixEpoch;
                return (ulong)(delta.Ticks / _timestep.Ticks);
            }

            public static int GenerateCode(SecurityToken securityToken, string modifier = null)
            {
                if (securityToken == null)
                {
                    throw new ArgumentNullException("securityToken");
                }

                // Allow a variance of no greater than 450 seconds in either direction
                var currentTimeStep = GetCurrentTimeStepNumber();
                using (var hashAlgorithm = new System.Security.Cryptography.HMACSHA1(securityToken.GetDataNoClone()))
                {
                    return ComputeTotp(hashAlgorithm, currentTimeStep, modifier);
                }
            }

            public static bool ValidateCode(SecurityToken securityToken, int code, string modifier = null)
            {
                if (securityToken == null)
                {
                    throw new ArgumentNullException("securityToken");
                }

                // Allow a variance of no greater than 450 seconds in either direction
                var currentTimeStep = GetCurrentTimeStepNumber();
                using (var hashAlgorithm = new System.Security.Cryptography.HMACSHA1(securityToken.GetDataNoClone()))
                {
                    for (var i = -2; i <= 2; i++)
                    {
                        var computedTotp = ComputeTotp(hashAlgorithm, (ulong)((long)currentTimeStep + i), modifier);
                        if (computedTotp == code)
                        {
                            return true;
                        }
                    }
                }

                // No match
                return false;
            }
        }

        #endregion
        #region SecurityToken

        private sealed class SecurityToken
        {
            private readonly byte[] _data;

            public SecurityToken(byte[] data)
            {
                _data = (byte[])data.Clone();
            }

            internal byte[] GetDataNoClone()
            {
                return _data;
            }
        }

        #endregion

        #region CreateSecurityToken

        private SecurityToken CreateSecurityToken(IAspNetUsers user)
        {
            return new SecurityToken(System.Text.Encoding.Unicode.GetBytes(user.SecurityStamp));
        }

        #endregion
        #region GetUserModifier

        protected virtual string GetUserModifier(string purpose, IAspNetUsers user)
        {
            return "Totp:" + purpose + ":" + user.Id;
        }

        #endregion

        #endregion
    }
}
